
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="robots" content="noindex">
    <title>animation-12</title>
    <style>
        body{background:black;margin:0;padding:0;overflow:hidden}
        canvas{background:transparent;background-image:linear-gradient(black 20%,#101 30%,#211 40%,#070702 52%,#000 90%,#000 100%);background-repeat:no-repeat;display:block;margin:0 auto;width:100%;;height:300px}
        #vignette{background-image:linear-gradient(right,black 0%,transparent 10%,transparent 90%,black 100%);position:absolute;top:0;left:50%;width:100%;height:300px;-webkit-transform:translateX(-50%);transform:translateX(-50%);z-index:50}
    </style>
</head>
<body>
<div id="vignette"></div>
<script>  console.clear()

// OVERENGINEERED UNOPTIMIZED CANVAS BULLSH*T
// BUT IT'S OKAY SINCE IT'S BLADE RUNNER INNIT
// Some stuff left unoptimized / verbose to show the work.
// TODO:
// - optimize render loop, avoid overdraws, etc
// - smoothly fade rows in on the horizon
// Constants.  Change at own risk
const CANVAS_WIDTH = screenWidth()
const CANVAS_HEIGHT = 300
const FRAME_TIME = 1000 / 16
const LIGHT_ROWS = 20
const LIGHT_ROW_DEPTH = 2
const LIGHT_SPACING = 0.6
const LIGHT_SIZE = 0.1
const LIGHT_SCATTER = 0.4
const BUILDING_ROWS = 38
const BUILDING_ROW_DEPTH = 1
const BUILDING_ROW_WIDTH = 60
const BUILDING_MIN_HEIGHT = 1.5
const BUILDING_MAX_HEIGHT = 3
const STACK_HEIGHT = 9
const STACK_THRESHOLD = 0.87
const STACK_LIGHT_CHANCE = 0.95
const STACK_LIGHT_SIZE = 0.13
const FADE_GRAY_VALUE = 25
const FADE_OFFSET = 0.35


// screen width
function screenWidth() {
    _width = document.body.offsetWidth;
    if(_width<600)
        _width=600
    if(_width>1200&&_width<1800)
        _width=1200
    if(_width>1800)
        _width=1800
    return _width;
}

// Virtual camera. Used in perspective calculations
const CAMERA = {
    x: 0,
    y: 10,
    z: 0,
    fov: 170,
    dist: 30,
    zSpeed: 0.005,
}

// Virtual vanishing point XY. Used in perspective calculations
const VP_OFS = {
    x: 0.5,
    y: 0.27,
}

// Global hoisted vars for rendering contexts and timers
let c, ctx, output_c, output_ctx
let _t, _dt, _ft

// Seedable random number generator.
// Not particularly well-distributed, but fine for this case.
// Allows us to emit the same set of random numbers on every frame
// so we can consistently re-render the scene.
const RNG = {
    seed: 1,
    random() {
        const x = Math.sin(RNG.seed++) * 10000
        return x - (x << 0)
    },
    randomInRange(min, max) {
        return ((RNG.random() * (max - min + 1)) << 0) + min
    }
}

// Module to get a random colour from a predefined list.
// Uses the seedable RNG
const Palette = (() => {
    const PAL = ['black', '#111', '#113', 'white', 'sliver', '#f88', 'orange', 'oldlace', '#569']
    const lastIndex = PAL.length - 1

    function getRandomFromPalette() {
        return PAL[RNG.randomInRange(0, lastIndex)]
    }

    return {
        getRandom: getRandomFromPalette
    }
})()

function ceil(n) {
    var f = (n << 0),
        f = f == n ? f: f + 1
    return f
}

// Update method of main loop
function update() {
    // Update our global timestamp (used in rendering)
    _t = Date.now() * 0.001
    // Move the camera slowly 'forward'
    CAMERA.z += CAMERA.zSpeed
}

// Draw a frame of the scene.
// Uses the current timestamp and the seeded RNG to render a
// pseudorandom cityscape with lights and buildings.
// We always generate and draw a set amount of city in front of
// the camera, so it appears to be endless as we 'fly over' it.
//
// 1. Clear the whole scene
// 2. Render random rows of lights
// 3. Render random rows of buildings
// 4. Blit scene to onscreen canvas
let _$ = {
    vPointX: 0,
    vPointY: 0,
    rowScreenX: 0,
    MAX_LIGHTS: 0,
    closestLightRow: 0,
    rowZ: 0,
    rowRelativeZ: 0,
    scalingFactor: 0,
    rowScreenWidth: 0,
    rowScreenHeight: 0,
    rowScreenY: 0,
    rowScreenLightSpacing: 0,
    rowLightCount: 0,
    lightSize: 0,
    lightHalfSize: 0,
    lightScreenX: 0,
    lightScreenY: 0,
    closestBuildingRow: 0,
    rowBuildingCount: 0,
    rowBuildingScreenWidth: 0,
    rowShade: 0,
    rowStyleString: '',
    lightData: [],
    isStack: false,
    buildingHeight: 0,
    buildingScreenHeight: 0,
    buildingScreenX: 0,
    buildingScreenY: 0,
    lightSize: 0,
    lightHalfSize: 0,
    lightColor: 0,
}
function render() {

    // Calculate the pixel XY of the vanishing point
    // (could be done on init, but useful if we ever want to
    // dynamically move the camera)
    _$.vPointX = c.width * VP_OFS.x >> 0
    _$.vPointY = c.height * VP_OFS.y >> 0

    // If we wanted to, we could give each row an X offset
    // and include it in perspective calculations,
    // but we just use the centre alignment for each one here.
    _$.rowScreenX = CAMERA.x + _$.vPointX

    // 1. Clear the whole scene...
    // (canvases are transparent so that the CSS 'sky' gradient can be seen)
    ctx.clearRect(0, 0, c.width, c.height)
    output_ctx.clearRect(0, 0, output_c.width, output_c.height)

    // 2. Render random rows of lights...
    // Calculate the closest row to the camera so we
    // can render the required number of rows into the distance
    _$.closestLightRow = Math.floor(CAMERA.z / LIGHT_ROW_DEPTH)

    // Draw each row of lights
    for (let i = 0; i < LIGHT_ROWS; i++) {

        // Calculate this row's base Z position
        // and Z relative to camera
        _$.rowZ = (_$.closestLightRow * LIGHT_ROW_DEPTH) + (LIGHT_ROW_DEPTH * i)
        _$.rowRelativeZ = _$.rowZ - CAMERA.z

        // Don't draw the row if it's behind the camera,
        // or beyond the camera's draw distance
        if (_$.rowRelativeZ <= 0 || _$.rowRelativeZ > CAMERA.dist) {
            continue
        }

        // Get the perspective scaling factor and pixel Y position for this row
        _$.scalingFactor = CAMERA.fov / _$.rowRelativeZ
        _$.rowScreenY = CAMERA.y * _$.scalingFactor + _$.vPointY

        // Don't draw the row if it's off-canvas
        if (_$.rowScreenY > c.height) {
            continue
        }

        // Calculate the spacing and number of lights we need to render for this row
        _$.rowScreenLightSpacing = LIGHT_SPACING * _$.scalingFactor
        _$.rowLightCount = c.width / _$.rowScreenLightSpacing

        // Seed the RNG in a way that gets us decent distribution
        // for the random lights
        RNG.seed = _$.rowZ * 0.573

        // Render the random lights for this row
        for (let j = 0; j < _$.rowLightCount; j++) {

            // Randomize light size, with perspective
            _$.lightSize = RNG.random() * (LIGHT_SIZE * _$.scalingFactor)
            _$.lightHalfSize = _$.lightSize * 0.5

            // Randomly offset the XY of the light, with perspective
            _$.lightScreenX = (j * _$.rowScreenLightSpacing) + (RNG.random() * LIGHT_SCATTER * _$.scalingFactor) - _$.lightHalfSize
            _$.lightScreenY = (_$.rowScreenY + (RNG.random() * LIGHT_SCATTER) * _$.scalingFactor) - _$.lightHalfSize

            // Don't render if the light is offscreen
            if (_$.lightScreenX < 0 || _$.lightScreenX > c.width || _$.lightScreenY > c.height) {
                // HACK: we still need to call the RNG the same number of times
                // for every row to ensure consistency between frames. If we didn't
                // do this, the lights would jump all over the place near the edges
                // of the screen.
                Palette.getRandom()
                continue
            }

            // Pick a random colour for this light
            ctx.fillStyle = Palette.getRandom()

            // Render the light twice, mirrored either side of the centre vanishing point.
            // Saves us having to do perspective offset calculation for every light,
            // and won't be noitceable when we overlay the city buildings.
            ctx.fillRect((_$.rowScreenX + _$.lightScreenX), _$.lightScreenY, _$.lightSize, _$.lightSize)
            ctx.fillRect((_$.rowScreenX - _$.lightScreenX), _$.lightScreenY, _$.lightSize, _$.lightSize)
        }
    }

    // 3. Render random rows of buildings...
    // Calculate the closest row to the camera so we
    // can render the required number of rows into the distance
    _$.closestBuildingRow = Math.floor(CAMERA.z / BUILDING_ROW_DEPTH)

    // Draw each row of buildings
    for (let i = BUILDING_ROWS; i > 0; i--) {

        // Calculate this row's base Z position
        // and Z relative to camera
        _$.rowZ = (_$.closestBuildingRow * BUILDING_ROW_DEPTH) + (BUILDING_ROW_DEPTH * i)
        _$.rowRelativeZ = _$.rowZ - CAMERA.z

        // Don't draw the row if it's behind the camera,
        // or beyond the camera's draw distance
        if (_$.rowRelativeZ <= 0 || _$.rowRelativeZ > CAMERA.dist) {
            continue
        }

        // Get the perspective scaling factor and pixel Y position for this row
        _$.scalingFactor = CAMERA.fov / _$.rowRelativeZ

        // Calculate the perspective-scaled position and base size of our row.
        // Offset the XY so that the row's 'origin' is at centre bottom (i.e. ground-up)
        _$.rowScreenWidth = BUILDING_ROW_WIDTH * _$.scalingFactor;
        _$.rowScreenHeight = BUILDING_MAX_HEIGHT * _$.scalingFactor;
        _$.rowScreenX = CAMERA.x * _$.scalingFactor + _$.vPointX - (_$.rowScreenWidth * 0.5)
        _$.rowScreenY = CAMERA.y * _$.scalingFactor + _$.vPointY - _$.rowScreenHeight

        // Seed the RNG to keep rendering consistent for this row
        RNG.seed = _$.rowZ

        // Calculate a random number of buildings for this row
        // and get their screen width
        _$.rowBuildingCount = RNG.randomInRange(20, 70)
        _$.rowBuildingScreenWidth = _$.rowScreenWidth / _$.rowBuildingCount

        // Calculate the shade we want the buildings in this row to be.
        // The tint is darker nearer the camera, giving a sort of crude distance fog
        // near the horizon.
        _$.rowShade = Math.round(FADE_GRAY_VALUE * (_$.rowRelativeZ / (CAMERA.dist) - FADE_OFFSET))
        _$.rowStyleString = 'rgb(' + _$.rowShade + ',' + _$.rowShade + ',' + _$.rowShade + ')'

        // Calclate and render each building
        _$.lightData.length = 0
        ctx.fillStyle = _$.rowStyleString
        for (let j = 0; j < _$.rowBuildingCount; j++) {

            // Buildings have a certain chance to become a 'stack' i.e. way taller than
            // everything else. We calculate a random ranged height for the building,
            // and if it exceeds a threshold, it gets turned into a stack.
            _$.isStack = false
            _$.buildingHeight = Math.max(BUILDING_MIN_HEIGHT, RNG.random() * BUILDING_MAX_HEIGHT)

            if (_$.buildingHeight > (BUILDING_MAX_HEIGHT * STACK_THRESHOLD)) {
                _$.isStack = true
                // Stacks have 40% height variance
                _$.buildingHeight = (STACK_HEIGHT * 0.6 + (RNG.random() * 0.4))
            }

            // Calculate the pixel size and position of this building, adjusted for perspective
            _$.buildingScreenHeight = _$.buildingHeight * _$.scalingFactor
            _$.buildingScreenX = _$.rowScreenX + (j * _$.rowBuildingScreenWidth)
            _$.buildingScreenY = _$.rowScreenY + _$.rowScreenHeight - _$.buildingScreenHeight

            // Draw the building on screen
            ctx.fillRect(_$.buildingScreenX, _$.buildingScreenY, Math.ceil(_$.rowBuildingScreenWidth), _$.buildingScreenHeight)

            // Seed the RNG for consistency when calculating stack lights (if needed)
            RNG.seed = _$.buildingHeight + j

            // Stacks have a chance to get lights on their top corners.
            // Generate and store light data so we can render it on top of the buildings
            if (_$.isStack && RNG.random() < STACK_LIGHT_CHANCE) {
                // Get random light size and color.
                // Slightly higher chance of red vs white lights
                _$.lightSize = RNG.random() * (STACK_LIGHT_SIZE * _$.scalingFactor)
                _$.lightColor = (RNG.random() > 0.6) ? 'white': 'red'
                // Save light info for rendering after we do all the buildings
                // (helps minimixe changes to ctx.fillStyle)
                _$.lightData.push(_$.buildingScreenX)
                _$.lightData.push(_$.buildingScreenY)
                _$.lightData.push(_$.lightSize)
                _$.lightData.push(_$.lightColor)
            }
        }

        // Draw any lights on stacks that need them in this row
        for (let j = 0; j < _$.lightData.length; j += 4) {
            _$.buildingScreenX = _$.lightData[j]
            _$.buildingScreenY = _$.lightData[j + 1]
            _$.lightSize = _$.lightData[j + 2]
            _$.lightHalfSize = _$.lightSize * 0.5
            _$.lightColor = _$.lightData[j + 3]

            // Draw lights centred at the top left and right corners of the stack
            ctx.fillStyle = _$.lightColor
            ctx.fillRect(_$.buildingScreenX - _$.lightHalfSize, _$.buildingScreenY - _$.lightHalfSize, _$.lightSize, _$.lightSize)
            ctx.fillRect(_$.buildingScreenX + _$.rowBuildingScreenWidth - _$.lightHalfSize, _$.buildingScreenY - _$.lightHalfSize, _$.lightSize, _$.lightSize)
        }
    }

    // 4. Blit scene to onscreen canvas.
    // Now that we've built up the scene in-memory, we just render the image to
    // our canvas in the DOM.
    output_ctx.drawImage(c, 0, 0)
}

// Main loop.
// Maintains a consistent update rate, but draws the screen as often
// as the browser will allow.
function frame() {
    requestAnimationFrame(frame)
    _ft = Date.now()
    update()
    if (_ft - _dt > FRAME_TIME) {
        render()
        _dt = _ft
    }

}

// Let's go!
function start() {
    // Init frame timers (see frame())
    _dt = _ft = Date.now()

    // Create two canvases - one for in-memory compositing,
    // and another to go in the DOM for our final render.
    // Make them the same size as each other.
    c = document.createElement('canvas')
    ctx = c.getContext('2d')

    output_c = document.createElement('canvas')
    output_ctx = output_c.getContext('2d')

    output_c.width = c.width = CANVAS_WIDTH
    output_c.height = c.height = CANVAS_HEIGHT
    document.body.appendChild(output_c)

    // Start the main loop.
    frame()
}

start()
</script>
</body>
</html>
